/*	--- AOP避坑指南 --- 


无论是【使用AspectJ语法】，还是【配合Annotation】，【使用AOP】，实际上就是让【Spring】自动为我们【创建一个Proxy】

使得【调用方】能无感知地【调用指定方法】，但【运行期】却【动态“织入”】了【其他逻辑】，因此，【AOP】本质上就是【一个代理模式】。

-----------------------------

因为【Spring】使用了【CGLIB】来实现【运行期动态创建Proxy】，如果我们没能【深入理解 其运行原理和实现机制】，就极有可能遇到【各种诡异的问题】。

我们来看一个【实际的例子】。

假设我们定义了【一个UserService的Bean】： */
@Component 
public class UserService {
	// 成员变量
	public final ZoneId zoneId = ZoneId.systemDefault();

	// 构造方法
	public UserService {
		System.out.println("UserService(): init...");
        System.out.println("UserService(): zoneId = " + this.zoneId);
	}

	// public方法
	public ZoneId getZoneId() {
		return ZoneId;
	}

	// public final方法
	public final ZoneId getFinalZoneId() {
		return zoneId;
	}
}

/*
再写个【MailService】，并【注入 UserService】： */
@Component 
public class MailService {
	@Autowired 
	UserService userService; // Inject UserService

	public String sendMail() {
		ZoneId zoneId = userService.zoneId;
		String dt = ZonedDateTime.now(zoneId).toString();

		return "Hello, it is " + dt;
	}
}


/*
最后用main()方法测试一下： */
@Configuration 
@ComponentScan
@EnableAspectJAutoProxy // 如果加入了 @Aspect @Component class的话
public class AppConfig {
	public static void main(String[] args) {
		ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
		MailService mailService = context.getBean(MailService.class);

		System.out.println(mailService.sendMail());
	}
}


/*
查看输出，一切正常：
--------------------------------------
UserService(): init...
UserService(): zoneId = Asia/Shanghai
Hello, it is 2020-04-12T10:23:22.917721+08:00[Asia/Shanghai]

--------------------------------------


下一步，我们给【UserService】加上【AOP支持】，就【添加一个最简单的 LoggingAspect】： */
@Aspect 
@Component 
public clss LoggingAspect {
	@Before("execution(public * com..*.UserService.*(..))")
	public void doAccessCheck() {
		System.out.println("[Before] do access check...");
	}
}

/*
别忘了在【AppConfig】上, 加上 
	@EnableAspectJAutoProxy。

再次运行，不出意外的话，会得到【一个NullPointerException】： 
-----------------------------------------
Exception in thread "main" java.lang.NullPointerException: zone
    at java.base/java.util.Objects.requireNonNull(Objects.java:246)
    at java.base/java.time.Clock.system(Clock.java:203)
    at java.base/java.time.ZonedDateTime.now(ZonedDateTime.java:216)
    at com.itranswarp.learnjava.service.MailService.sendMail(MailService.java:19)
    at com.itranswarp.learnjava.AppConfig.main(AppConfig.java:21)

-----------------------------------------------------


仔细跟踪【代码】，会发现【null值】出现在【MailService.sendMail()内部】的【这一行代码】：
*/
@Component
public class MailService {
    @Autowired
    UserService userService;

    public String sendMail() {
        ZoneId zoneId = userService.zoneId;
        System.out.println(zoneId); // null
        ...
    }
}

/*
我们还故意在【UserService】中, 特意用[final]修饰了一下[成员变量]： */
@Component
public class UserService {
    public final ZoneId zoneId = ZoneId.systemDefault();
    ...
}

/*
用final标注的成员变量为null？逗我呢？ 

怎么肥四？

为什么加了【AOP】就报【NPE】，【去了AOP】就一切正常？

【final字段】不执行，难道【JVM】有问题？

为了解答这个诡异的问题，我们需要深入理解【Spring】使用【CGLIB 生成Proxy的原理】：

	第1步，正常创建【一个UserService的原始实例】，这是通过【反射】调用【构造方法】实现的，它的行为和我们预期的完全一致；

	第2步，通过【CGLIB】创建【一个UserService的子类】，并【引用】了【原始实例】和【LoggingAspect】： 
*/
public UserService$$EnhancerBySpringCGLIB extends UserService {

    UserService target;   // 引用@Component  UserService
    LoggingAspect aspect; // 引用@Aspect  LogginAspect
 
    public UserService$$EnhancerBySpringCGLIB() {
    }

    public ZoneId getZoneId() {
        aspect.doAccessCheck();
        return target.getZoneId();
    }
}

/*
如果我们观察【Spring创建的AOP代理】，【它的类名】总是类似【UserService$$EnhancerBySpringCGLIB$$1c76af9d】（你没看错，Java的类名实际上允许$字符）。

为了让【调用方】获得【UserService的引用】，【代理类】必须继承自【UserService】。

然后，【该代理类】会覆写【所有public和protected方法】，并在【内部】将【调用】委托给【原始的UserService实例】。

这里出现了【两个UserService实例】：

一个是【我们代码中定义的原始实例】，【原始实例の成员变量】已经按照我们预期的方式【被初始化完成】： */
UserService original = new UserService();

/*
【第2个UserService实例】, 实际上【类型】是【UserService$$EnhancerBySpringCGLIB】，【它】引用了【原始的UserService实例】： */
UserService$$EnhancerBySpringCGLIB proxy = new UserService$$EnhancerBySpringCGLIB();
proxy.target = original;
proxy.aspect = ...;


/*
注意到【这种情况】仅出现在【启用了AOP的情况】，此刻，从【ApplicationContext】中获取的【UserService实例】是【proxy】，注入到【MailService】中的【UserService实例】也是【proxy】。

那么最终的问题来了：【proxy实例 的 成员变量】，也就是【从UserService继承的zoneId】，zoneId的值 是 null。

原因在于，【UserService成员变量】的【初始化】： */
public class UserService {
	public final ZoneId zoneId = ZoneId.systemDefault();
	//...
}

/*
在【UserService$$EnhancerBySpringCGLIB】中，并【未执行】。

原因是，【没必要初始化proxy的成员变量】，因为【proxy的目的】是【代理Method, 对Field无视】。

实际上，【成员变量的初始化】是在【构造方法】中完成的。

这是我们看到的代码： */
public class UserService {
	public final ZoneId zoneId = ZoneId.systemDefault();

	public UserService() {
		//...
	}
}


/*
这是【编译器】实际【编译的代码】： */
public class UserService {
	public final ZoneId zoneId;

	public UserService() {
		super(); // 构造方法第一行代码总是调用super()
		zoneId = ZoneId.systemDefault(); // 继续初始化成员变量
	}
}


/*
然而，对于【Spring】通过【CGLIB】动态创建的【UserService$$EnhancerBySpringCGLIB代理类】，它的构造方法中，并【未调用super()】，因此，【从父类继承的成员变量】，包括【final类型的成员变量】，统统都【没有初始化】。


Q： 有的童鞋会问：Java语言规定，任何类的构造方法，第一行必须调用super()，如果没有，编译器会自动加上，怎么Spring的CGLIB就可以搞特殊？

A： 这是因为【自动加super()的功能】是【Java编译器】实现的，它发现你没加，就自动给加上，发现你加错了，就报编译错误。

但实际上，如果【直接构造字节码】，【一个类.构造方法】中，不一定非要调用super()。

【Spring】使用【CGLIB 构造的Proxy类】，是【直接生成字节码】，并没有【源码-编译-字节码】这个步骤，因此：

PS:  【Spring】通过【CGLIB】创建的【代理类】，【不会初始化】【代理类自身继承.任何成员变量】，包括【final类型的成员变量】！


-------------------------------

再考察MailService的代码： */
@Component 
public class MailService {
	@Autowired 
	UserService userService;

	public String sendMail() {
		ZoneId zoneId = userService.zoneId;
		System.out.println(zoneId);
		//...
	}
}

/*
如果【没有启用AOP】，注入的是【原始的UserService实例】，那么一切正常，因为【UserService实例.zoneId字段】已经被【正确初始化】了。

如果【启动了AOP】，注入的是【代理后的UserService$$EnhancerBySpringCGLIB实例】，那么问题大了：【获取的UserService$$EnhancerBySpringCGLIB实例.zoneId字段】，【永远为null】。

那么问题来了：启用了AOP，如何修复？

【修复】很简单，只需要把【直接访问class.Field的代码】，改为通过【class.Method访问】： */
@Component 
public class MailService {
	@Autowired 
	UserService userService;

	public String snedMail() {
		// 不要直接访问UserService.Field
		ZoneId zoneId = userService.getZoneId();
			// 通过class.getField()获取
		//...
	} 
}

/*
无论【注入的UserService】是【原始实例】还是【代理实例】，【getZoneId()】都能【正常工作】，因为【代理类】会【覆写getZoneId()方法】，并将【代理覆写的方法】委托给【原始实例】： */
public UserService$$EnhancerBySpringCGLIB extends UserService {

	UserService target = ...;

	public ZoneId getZoneId() {
		return target.getZoneId(); // 会正确覆写
	}
}

/*
注意到我们还给【UserService】添加了【一个public + final的方法】： */
@Component 
public class UserService {
	//..

	public final ZoneId getFinalZoneId() {
		return zoneId;
	}
}

/*
如果在[MailService]中，调用的[不是getZoneId()]，而是[getFinalZoneId()]，又会[出现NullPointerException]

这是因为，【代理类】无法覆写【final方法】（这一点绕不过[JVM的ClassLoader检查], final卡死了CGLIB的覆写）

【该final方法】返回的是【代理类的zoneId字段，即null】。

实际上，如果我们【加上日志】，【Spring】在【启动时】, 会打印【一个警告】：
--------------------------------
10:43:09.929 [main] DEBUG org.springframework.aop.framework.CglibAopProxy - Final method [public final java.time.ZoneId xxx.UserService.getFinalZoneId()] cannot get proxied via CGLIB: Calls to this method will NOT be routed to the target instance and might lead to NPEs against uninitialized fields in the proxy instance.
-----------------------------------

上面的日志大意就是：
  因为【被代理的UserService】有【一个final方法getFinalZoneId()】，这会导【致其他Bean】如果【调用此方法】，【无法将其】代理到【真正的原始实例】，从而【可能发生NPE异常】。

-------

因此，【正确使用AOP】，我们需要【一个避坑指南】：

1. 访问【被注入的Bean】时，总是【调用方法】而非直接访问字段；
2. 【编写Bean】时，如果【可能会被代理】，就【不要编写public final方法】。

这样才能保证【有没有AOP，代码都能正常工作】。


================================================


思考

为什么【Spring】刻意不初始化【Proxy继承的字段】？

如果【一个Bean】不允许【任何AOP代理】，应该【怎么做】来“保护”自己【在运行期】不会【被代理】？



===============================================

#	----- AOP避坑指南 の 小结 ----- 


1. 由于【Spring】通过【CGLIB】实现【代理类】，我们要避免【直接访问Bean的字段】，以及【由final方法】带来的【“未代理”问题】。

2. 遇到【CglibAopProxy的相关日志】，【务必要仔细检查】，防止【因为AOP出现NPE异常】。


*/